Linked Lists
A linked list is a data structure that, similar to an array, stores elements in an ordered sequence. The main difference is that linked lists are made up of objects called ListNode
s (nodes).
A ListNode
is an object that contains two attributes:
value
- this stores the value of the node. It could be a character, an integer, another object, anything, etc.next
- this stores the reference to the next node (the next node’s address in memory) in the linked list.
→ this is the way to connect multiple ListNode
s together.
1. Creating a Linked List
By chaining these ListNode
objects together we can build a linked list.
class ListNode:
def __init__(self, val):
self.val = val # the actual value
self.next = None # reference to the next node
Example: We have three ListNode
objects - ListNode1
, ListNode2
, ListNode3
.
In this example:
value
attribute is a string - red, blue, green.- We need to make sure that our
next
pointers point to anotherListNode
, and not null. Only the last node in the linked list would have itsnext
pointer point tonull
.
ListNode1.next = ListNode2
ListNode2.next = ListNode3
ListNode3.next = null
When we instantiate a list node, we don’t know where it is stored in memory. The nodes likely won’t be contiguous like arrays, but this isn’t an issue for linked list.
2. Traversal
To traverse a linked list from beginning to end, we can make use of a while
loop.
- We start the traverse at the head of the list,
ListNode1
. - We assign it to a variable
current
, denoting the current node we are at. - We execute the
while
loop until we each the end of the list, which isnull
. - In each iteration, we update
current
to be the next node in the list by settingcurrent = current.next
. → The traversal runs inO(n)
time where n is the number of nodes in the linked list.
current = ListNode1
while cur:
current = current.next
3. [Circular Linked List](https://www.youtube.com/watch?v=clh4QgiXjlc
If ListNode3
’s next
pointer is set to ListNode1
instead of null
, this results in a circular linked list
. Attempting to iterate through a circular linked list would result in an infinite loop.
I. Singly Linked Lists
1. Appending Operations
An advantage that linked lists have over arrays is that inserting a new element can be performed in O(1)
time, even if we insert in the middle.
- We do not have to shift any elements since there is no requirement for the elements to be stored contiguously in memory.
This assumes that we’d already have a reference to the node at the desired position we want to insert. If we have to traverse the list to arrive at the insertion point, the operation would take
O(n)
time.
Example: If we wanted to append ListNode4
to the end of the list, we would be appending to the tail.
- Once
ListNode4
is appended, we update our tail pointer to beListNode4
. → This operation would be done inO(1)
.
tail.next = ListNode4
tail = ListNode4
2. Deleting from a Singly Linked List
Deleting a node from a singly linked list will take O(1)
since we can accomplish this by updating a single pointer, again, we’re assuming we have a reference to the node at the desired position we want to delete.
Example: We want to delete ListNode2
.
- Currently, our
head
points toListNode1
, andhead.next
points toListNode2
. - We can update
head.next
pointer toListNode3
, by chainingnext
pointers likehead.next.next
.
head.next = head.next.next
It can be assumed that the memory occupied by
ListNode2
will be cleared via garbage collection in most languages. In other languages like C, you would have to manually free the memory.
Summary
Time Complexity
II. Doubly Linked List
A node in a doubly linked list now has two pointers, in addition to the next
pointer, we have a prev
pointer which points to the previous node. If prev
points to null
, that means we’re at the head of the linked list.
1. Insert Operations
a. Insertion End
Similar to the singly linked list, adding a node to a doubly linked list will run in O(1)
time. Only this time, we have to update the prev
pointer as well.
Example:
We have three nodes in our linked list, ListNode1
, ListNode2
, ListNode3
.
If we want to insert at the end another node, ListNode4
, we will have to update both the next
pointer of ListNode3
and the prev
pointer of the new node, ListNode4
.
tail.next = ListNode4
ListNode4.prev = tail
tail = tail.next
b. Insertion Front
To add a node to the head of a doubly linked list:
- We update the
next
pointer of the new nodeListNode4
to the current head -ListNode1
. - We update the
prev
pointer ofListNode1
to the new node -ListNode4
.
ListNode4.next = head
head.prev = ListNode4
2. Delete Operations
a. Deletion End
Deleting at the end is also a O(1)
operation:
- First we get a reference to the node before the tail.
- We update the
next
pointer of the node before the tail tonull
. - We update the tail to be the node before the tail.
- (Optional) We can also update the
prev
pointer of the old tail tonull
.
# get the node before tail
ListNode2 = tail.prev
# set this node's next pointer to null
ListNode2.next = null
tail = ListNode2 # ListNode2 becomes the new tail
b. Deletion Front
To delete a head node:
- We get the reference to the second node (right after the head).
- We update the second node’s
prev
pointer tonull
head.next.prev = null
head = head.next # new head of the linked list
3. Access
Similar to singly linked lists, we cannot randomly access a node. So in the worst case, we will have to traverse n
nodes before reaching the desired node. This would run in O(n)
time.
Doubly linked lists have the benefit that we can traverse the list in both directions, as opposed to singly linked lists.